package ace.cmp.spring.data.jdbc.querydsl.core.impl;

import ace.cmp.spring.data.jdbc.querydsl.core.LogicDeleteHelper;
import ace.cmp.spring.data.jdbc.querydsl.core.QuerydslLogicNotDeleteFlagValueGetter;
import ace.cmp.spring.data.jdbc.querydsl.core.QuerydslRelationHelper;
import ace.cmp.spring.data.jdbc.querydsl.core.annotations.LogicDelete;
import com.google.common.base.CaseFormat;
import com.querydsl.core.types.Path;
import com.querydsl.core.types.dsl.SimpleExpression;
import com.querydsl.sql.RelationalPathBase;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import lombok.SneakyThrows;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.util.ReflectionUtils;

/**
 * @author caspar
 * @date 2023/8/25 9:51
 */
public class QuerydslRelationHelperImpl implements QuerydslRelationHelper {

  private final RelationalMappingContext mappingContext;
  private final ApplicationContext applicationContext;
  private final QuerydslLogicNotDeleteFlagValueGetter defaultQuerydslLogicNotDeleteFlagValueGetter;
  private final LogicDeleteHelper logicDeleteHelper;

  public QuerydslRelationHelperImpl(
      RelationalMappingContext mappingContext,
      ApplicationContext applicationContext,
      QuerydslLogicNotDeleteFlagValueGetter querydslLogicNotDeleteFlagValueGetter,
      LogicDeleteHelper logicDeleteHelper) {
    this.mappingContext = mappingContext;
    this.applicationContext = applicationContext;
    this.defaultQuerydslLogicNotDeleteFlagValueGetter = querydslLogicNotDeleteFlagValueGetter;
    this.logicDeleteHelper = logicDeleteHelper;
  }

  @SneakyThrows
  @Override
  public <Id, T> Id getEntityId(T entity) {
    Id id =
        (Id)
            this.mappingContext
                .getPersistentEntity(entity.getClass())
                .getIdProperty()
                .getGetter()
                .invoke(entity, null);
    return id;
  }

  @SneakyThrows
  @Override
  public <Id, T> void setEntityId(T entity, Id id) {
    this.mappingContext
        .getPersistentEntity(entity.getClass())
        .getIdProperty()
        .getSetter()
        .invoke(entity, id);
  }

  @Override
  public <T> RelationalPathBase<T> getRelationalPathBaseByEntityType(Class<T> entityType) {
    if (entityType == null) {
      throw new IllegalArgumentException("entityType can not be null");
    }

    return getRelationalPathBaseFromQueryClass(getQueryClassByEntityType(entityType));
  }

  @Override
  public <T> Class<T> getQueryClassByEntityType(Class<T> entityType) {
    var fullName = entityType.getPackage().getName() + ".Q" + entityType.getSimpleName();
    try {
      return (Class<T>) entityType.getClassLoader().loadClass(fullName);
    } catch (ClassNotFoundException e) {
      throw new IllegalArgumentException("Unable to load class " + fullName);
    }
  }

  @Override
  public <T> RelationalPathBase<T> getRelationalPathBaseFromQueryClass(Class<T> queryClass) {
    var fieldName =
        CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, queryClass.getSimpleName().substring(1));
    var field = ReflectionUtils.findField(queryClass, fieldName);

    if (field == null) {
      throw new IllegalArgumentException(
          "Did not find a static field of the same type in " + queryClass);
    }

    return (RelationalPathBase<T>) ReflectionUtils.getField(field, null);
  }

  @Override
  public <T> RelationalPersistentEntity<T> getRelationalPersistentEntityByEntityType(
      Class<T> entityType) {
    return (RelationalPersistentEntity<T>)
        this.mappingContext.getRequiredPersistentEntity(entityType);
  }

  /**
   * 获取主键查询表达式
   *
   * @return
   */
  @Override
  public <T> List<SimpleExpression> getPrimaryKeyExpressions(Class<T> entityType) {
    RelationalPathBase<T> entityPath = this.getRelationalPathBaseByEntityType(entityType);
    if (entityPath.getPrimaryKey() == null
        || entityPath.getPrimaryKey().getLocalColumns().isEmpty()) {
      return Collections.EMPTY_LIST;
    }
    return entityPath.getPrimaryKey().getLocalColumns().stream()
        .map(propertyPath -> (SimpleExpression) propertyPath)
        .collect(Collectors.toList());
  }

  @Override
  public <T> SimpleExpression getPrimaryKeyExpression(Class<T> entityType) {
    return this.getPrimaryKeyExpressions(entityType).get(0);
  }

  @Override
  public <T> Path getLogicDeletePath(Class<T> entityType) {
    Field logicDeleteField = this.getLogicDeleteField(entityType);
    if (logicDeleteField == null) {
      return null;
    }
    Path path = this.getPathByEntityTypeAndFieldName(entityType, logicDeleteField.getName());
    if (path == null) {
      throw new RuntimeException(
          "LogicDelete cannot find RelationalPath by field name" + logicDeleteField.getName());
    }
    return path;
  }

  @Override
  public SimpleExpression getLogicDeleteExpression(Class entityType) {
    Field logicDeleteField = this.getLogicDeleteField(entityType);
    if (logicDeleteField == null) {
      return null;
    }
    SimpleExpression simpleExpression =
        (SimpleExpression)
            this.getPathByEntityTypeAndFieldName(entityType, logicDeleteField.getName());
    if (simpleExpression == null) {
      throw new RuntimeException(
          "LogicDelete cannot find SimpleExpression by field name" + logicDeleteField.getName());
    }
    return simpleExpression;
  }

  @Override
  public <T> Path getPrimaryKeyPath(Class<T> entityType) {
    return this.getRelationalPathBaseByEntityType(entityType)
        .getPrimaryKey()
        .getLocalColumns()
        .get(0);
  }

  @Override
  public <T> Path getPathByEntityTypeAndFieldName(Class<T> entityType, String fieldName) {
    RelationalPathBase<T> entityPath = this.getRelationalPathBaseByEntityType(entityType);
    return entityPath.getColumns().stream()
        .filter(p -> p.getMetadata().getName().equals(fieldName))
        .findFirst()
        .orElse(null);
  }

  @Override
  public Path getPathByEntityTypeAndAnnotation(Class<?> entityType,
      Class<? extends Annotation> annClass) {
    Field field = this.getFieldByAnnotation(entityType, annClass);
    if (Objects.isNull(field)) {
      return null;
    }
    return this.getPathByEntityTypeAndFieldName(entityType, field.getName());
  }

  @Override
  public Object getLogicNotDeleteFlagValue(Class entityType) {
    Field logicDeleteField = this.getLogicDeleteField(entityType);
    if (logicDeleteField == null) {
      return null;
    }
    LogicDelete logicDeleteAnnotation = logicDeleteField.getAnnotation(LogicDelete.class);
    QuerydslLogicNotDeleteFlagValueGetter logicNotDeleteFlagValueGetter;
    if (logicDeleteAnnotation == null
        || logicDeleteAnnotation.logicNotDeleteValueGetter() == null) {
      logicNotDeleteFlagValueGetter = this.defaultQuerydslLogicNotDeleteFlagValueGetter;
    } else {
      logicNotDeleteFlagValueGetter =
          this.applicationContext.getBean(logicDeleteAnnotation.logicNotDeleteValueGetter());
    }
    return logicNotDeleteFlagValueGetter.get(entityType, logicDeleteAnnotation);
  }

  @Override
  @SneakyThrows
  public Object getLogicDeleteFieldValue(Object object) {
    return this.logicDeleteHelper.getLogicDeleteFieldValue(object);
  }

  @Override
  public Field getLogicDeleteField(Class entityType) {
    return this.logicDeleteHelper.getLogicDeleteField(entityType);
  }

  @Override
  public Field getFieldByAnnotation(Class<?> entityType, Class<? extends Annotation> annClass) {
    List<Field> fields = FieldUtils.getFieldsListWithAnnotation(entityType, annClass);
    if (fields.size() > 1) {
      throw new RuntimeException("field cannot more than 1");
    }
    if (CollectionUtils.isEmpty(fields)) {
      return null;
    }
    return fields.get(0);
  }

  @Override
  public Field getFieldByName(Class<?> entityType, String name) {
    return FieldUtils.getField(entityType, name,true);
  }
}
